﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Diagnostics;
using System.ComponentModel;
using System.Collections;
using System.Reflection;
using System.Collections.Specialized;

namespace CodeDomVisitorGenerator
{
    class CodeDomVisitorGenerator
    {
        static List<Type> _ignoreTypes = new List<Type>() { 
            typeof(object),
            typeof(string),
            typeof(StringCollection),
            typeof(CodeLinePragma),
            typeof(CodeDirectiveCollection),
            typeof(CodeObject),
        };
        static Type _codeObjectType = typeof(CodeObject);
        static Dictionary<Type, CodeMethodReferenceExpression> _generatedVisitMethod = new Dictionary<Type, CodeMethodReferenceExpression>();
        static CodeDomProvider _provider;
        static CodeGeneratorOptions _options;
        static CodeTypeDeclaration _containerType;
        static CodeStatementCollection _rootVisitMethodBody;
        static TextWriter _writer;

        static void Main(string[] args)
        {
            if (args.Length != 2)
            {
                Usage();
                return;
            }

            string typeName = args[0];
            int splitterIndex = typeName.LastIndexOf(".");
            string namespaceName = typeName.Substring(0, splitterIndex);
            typeName = typeName.Substring(splitterIndex + 1);
            _writer = new StreamWriter(File.Open(args[1], FileMode.Create));

            Debug.Listeners.Add(new ConsoleTraceListener());
            Debug.Listeners.Add(new TextWriterTraceListener(File.Create("test.log")));

            //
            // Gets Generator
            //
            _provider = CodeDomProvider.CreateProvider("CSharp");
            _options = new CodeGeneratorOptions();
            _options.BlankLinesBetweenMembers = true;
            _options.IndentString = "    ";
            _options.BracingStyle = "C";
            _options.ElseOnClosing = true;

            //
            // Generate Comments
            //
            _provider.GenerateCodeFromStatement(new CodeCommentStatement("Generated by CodeDomVisitorGenerator @ Colin Han")
                , _writer, _options);
            _provider.GenerateCodeFromStatement(new CodeCommentStatement("------------------------------------------------")
                , _writer, _options);

            //
            // Create namesapce
            //
            CodeCompileUnit file = new CodeCompileUnit();
            CodeNamespace @namespace = new CodeNamespace(namespaceName);
            file.Namespaces.Add(@namespace);

            //
            // Create type
            //
            _containerType = new CodeTypeDeclaration(typeName);
            _containerType.TypeAttributes = TypeAttributes.NotPublic;
            _containerType.CustomAttributes.Add(
                new CodeAttributeDeclaration(
                    new CodeTypeReference(typeof(GeneratedCodeAttribute)),
                    new CodeAttributeArgument(
                        new CodePrimitiveExpression(typeof(CodeDomVisitorGenerator).Assembly.GetName().Name)
                    ),
                    new CodeAttributeArgument(
                        new CodePrimitiveExpression(typeof(CodeDomVisitorGenerator).Assembly.GetName().Version.ToString())
                    )
                )
            );
            @namespace.Types.Add(_containerType);

            //
            // Create Visit(CodeObject) method
            //
            _rootVisitMethodBody = CreateRootVisitMethodBody();

            //
            // Generate Other VisitMethods
            //
            foreach (Type type in _codeObjectType.Assembly.GetTypes())
            {
                if (_codeObjectType != type && _codeObjectType.IsAssignableFrom(type))
                {
                    if (!_generatedVisitMethod.ContainsKey(type))
                    {
                        Debug.WriteLine("Generate Visit To: " + type.FullName + " {");
                        Debug.IndentLevel++;
                        CreateVisitMethod(type);
                        Debug.IndentLevel--;
                        Debug.WriteLine("}");
                    }
                }
            }

            //
            // Generate dispatch code
            //
            var dispatchType = _generatedVisitMethod.Keys.Where((t) => _codeObjectType.IsAssignableFrom(t) && _codeObjectType != t);
            dispatchType = dispatchType.OrderBy((t) => GetTypeCompareValue(t)).Reverse();
            foreach (Type type in dispatchType)
            {
                Debug.WriteLine("Generic Dispatcher of " + type.FullName);

                _rootVisitMethodBody.Add(
                    new CodeConditionStatement(
                        CreateIsExpression(
                            new CodeArgumentReferenceExpression("code"),
                            new CodeTypeReferenceExpression(type)
                        ),
                        new CodeExpressionStatement(
                            new CodeMethodInvokeExpression(
                                _generatedVisitMethod[type],
                                CreateAsExpression(
                                    new CodeArgumentReferenceExpression("code"),
                                    new CodeTypeReferenceExpression(type)
                                )
                            )
                        )
                    )
                );

                _rootVisitMethodBody.Add(new CodeSnippetStatement());
            }

            //
            // Generate Code
            //
            _provider.GenerateCodeFromCompileUnit(file, _writer, _options);

            _writer.Close();

            Debug.Flush();
            Console.ReadLine();
        }

        private static int GetTypeCompareValue(Type type)
        {
            int value = 0;
            while (type == null)
            {
                value++;
                type = type.BaseType;
            }
            return value;
        }

        private static CodeStatementCollection CreateRootVisitMethodBody()
        {
            CodeMemberMethod rootVisitMethod = new CodeMemberMethod();
            rootVisitMethod.Name = "Visit";
            rootVisitMethod.ReturnType = null;
            rootVisitMethod.Attributes = MemberAttributes.Public | MemberAttributes.Overloaded;
            rootVisitMethod.Parameters.Add(new CodeParameterDeclarationExpression(new CodeTypeReference(typeof(CodeObject)), "code"));
            _containerType.Members.Add(rootVisitMethod);
            _generatedVisitMethod.Add(typeof(CodeObject), 
                new CodeMethodReferenceExpression(
                    new CodeThisReferenceExpression(),
                    "Visit"
                )
            );
            return rootVisitMethod.Statements;
        }
        private static CodeMemberMethod CreateBasicVisitMethod(Type type)
        {
            Debug.Assert(!_generatedVisitMethod.ContainsKey(type));

            CodeMemberMethod visitMethod = new CodeMemberMethod();
            visitMethod.Name = "Visit";
            visitMethod.ReturnType = null;
            visitMethod.Attributes = MemberAttributes.Family | MemberAttributes.Overloaded;
            visitMethod.Parameters.Add(new CodeParameterDeclarationExpression(new CodeTypeReference(type), "code"));
            _containerType.Members.Add(visitMethod);
            _generatedVisitMethod.Add(type,
                new CodeMethodReferenceExpression(
                    new CodeThisReferenceExpression(),
                    "Visit"
                )
            );

            return visitMethod;
        }
        private static CodeMethodReferenceExpression GetVisitMethodReferenceExpression(Type type)
        {
            if (!_generatedVisitMethod.ContainsKey(type))
            {
                Debug.WriteLine("Generate Visit To: " + type.FullName + " {");
                Debug.IndentLevel++;
                if (typeof(IList).IsAssignableFrom(type))
                {
                    CreateCollectionVisitMethod(type);
                }
                else
                {
                    CreateVisitMethod(type);
                }
                Debug.IndentLevel--;
                Debug.WriteLine("}");
            }
            return _generatedVisitMethod[type];
        }

        private static void CreateCollectionVisitMethod(Type type)
        {
            CodeMemberMethod method = CreateBasicVisitMethod(type);

            PropertyInfo indexer = type.GetProperty("Item", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
            Type itemType = indexer.PropertyType;

            method.Statements.Add(
                new CodeIterationStatement(
                    new CodeVariableDeclarationStatement(
                        new CodeTypeReference(typeof(int)),
                        "i",
                        new CodePrimitiveExpression(0)
                    ),
                    new CodeBinaryOperatorExpression(
                        new CodeVariableReferenceExpression("i"),
                        CodeBinaryOperatorType.LessThan,
                        new CodePropertyReferenceExpression(
                            new CodeArgumentReferenceExpression("code"),
                            "Count"
                        )
                    ),
                    new CodeAssignStatement(
                        new CodeVariableReferenceExpression("i"),
                        new CodeBinaryOperatorExpression(
                            new CodeVariableReferenceExpression("i"),
                            CodeBinaryOperatorType.Add,
                            new CodePrimitiveExpression(1)
                        )
                    ),
                    new CodeExpressionStatement(
                        new CodeMethodInvokeExpression(
                            GetVisitMethodReferenceExpression(itemType),
                            CreateAsExpression(
                                new CodeIndexerExpression(
                                    new CodeArgumentReferenceExpression("code"),
                                    new CodeVariableReferenceExpression("i")
                                ),
                                new CodeTypeReferenceExpression(itemType)
                            )
                        )
                    )
                )
            );
        }
        private static void CreateVisitMethod(Type type)
        {
            CodeMemberMethod method = CreateBasicVisitMethod(type);

            //
            // Visit base
            //
            if (ShouldGenerate(type.BaseType))
            {
                Debug.WriteLine("Visit To Base");
                method.Statements.Add(
                    new CodeExpressionStatement(
                        new CodeMethodInvokeExpression(
                            GetVisitMethodReferenceExpression(type.BaseType),
                            CreateAsExpression(
                                new CodeArgumentReferenceExpression("code"),
                                new CodeTypeReferenceExpression(type.BaseType)
                            )
                        )
                    )
                );

                method.Statements.Add(new CodeSnippetStatement());
            }

            //
            // Visit members
            //
            foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                if (property.DeclaringType != type)
                {
                    continue;
                }
                if (!ShouldGenerate(property.PropertyType))
                {
                    Debug.WriteLine("Ignore a Property: " + property.Name + "(" + property.PropertyType.FullName + ")");
                    continue;
                }
                Debug.WriteLine("Visit To Property: " + property.Name);
                method.Statements.Add(
                    new CodeConditionStatement(
                        new CodeBinaryOperatorExpression(
                            new CodePropertyReferenceExpression(
                                new CodeArgumentReferenceExpression("code"),
                                property.Name
                            ),
                            CodeBinaryOperatorType.IdentityInequality,
                            new CodePrimitiveExpression(null)
                        ),
                        new CodeExpressionStatement(
                            new CodeMethodInvokeExpression(
                                GetVisitMethodReferenceExpression(property.PropertyType),
                                _codeObjectType.IsAssignableFrom(property.PropertyType) ?
                                    CreateAsExpression(
                                        new CodePropertyReferenceExpression(
                                            new CodeArgumentReferenceExpression("code"),
                                            property.Name
                                        ),
                                        new CodeTypeReferenceExpression(_codeObjectType)
                                    ) :
                                    new CodePropertyReferenceExpression(
                                        new CodeArgumentReferenceExpression("code"),
                                        property.Name
                                    )
                            )
                        )
                    )
                );
            }

        }

        private static bool ShouldGenerate(Type type)
        {
            if (!type.IsClass || type.IsArray)
            {
                return false;
            }
            return !_ignoreTypes.Contains(type);
        }

        private static CodeExpression CreateIsExpression(CodeExpression variable, CodeTypeReferenceExpression type)
        {
            StringBuilder sb = new StringBuilder();
            TextWriter writer = new StringWriter(sb);
            _provider.GenerateCodeFromExpression(variable, writer, _options);
            writer.Write(" is ");
            _provider.GenerateCodeFromExpression(type, writer, _options);
            return new CodeSnippetExpression(sb.ToString());
        }
        private static CodeExpression CreateAsExpression(CodeExpression variable, CodeTypeReferenceExpression type)
        {
            StringBuilder sb = new StringBuilder();
            TextWriter writer = new StringWriter(sb);
            _provider.GenerateCodeFromExpression(variable, writer, _options);
            writer.Write(" as ");
            _provider.GenerateCodeFromExpression(type, writer, _options);
            return new CodeSnippetExpression(sb.ToString());
        }

        private static void Usage()
        {
            Console.WriteLine("CodeDomVisitorGenerator.exe namespace.typeName fileName");
        }
    }
}
